package com.xiaopo.flying.puzzle;

import com.xiaopo.flying.puzzle.ext.PointF;
import com.xiaopo.flying.puzzle.ext.RectF;
import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.agp.components.DragEvent;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.render.Texture;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.TouchEvent;

import static com.xiaopo.flying.puzzle.MatrixUtils.calculateImageIndents;
import static com.xiaopo.flying.puzzle.MatrixUtils.getMinMatrixScale;
import static com.xiaopo.flying.puzzle.MatrixUtils.judgeIsImageContainsBorder;

/**
 * @author wupanjie
 */
@SuppressWarnings("WeakerAccess")
public class PuzzlePiece {
    private Element drawable;
    private Matrix matrix;
    private Matrix previousMatrix;
    private Area area;
    private Rect drawableBounds;
    private float[] drawablePoints;
    private float[] mappedDrawablePoints;

    private float previousMoveX;
    private float previousMoveY;

    private final RectF mappedBounds;
    private final PointF centerPointF;
    private final PointF mappedCenterPointF;

    private AnimatorValue animator;
    private int duration = 300;
    private Matrix tempMatrix;
    private String path = "";

    PuzzlePiece(Element drawable, Area area, Matrix matrix) {
        this.drawable = drawable;
        this.area = area;
        this.matrix = matrix;
        this.previousMatrix = new Matrix();
        this.drawableBounds = new Rect(0, 0, getWidth(), getHeight());
        this.drawablePoints = new float[]{
            0f, 0f, getWidth(), 0f, getWidth(), getHeight(), 0f, getHeight()
        };
        this.mappedDrawablePoints = new float[8];

        this.mappedBounds = new RectF();
        this.centerPointF = new PointF(area.centerX(), area.centerY());
        this.mappedCenterPointF = new PointF();
        this.animator = new AnimatorValue();
        this.animator.setCurveType(Animator.CurveType.ACCELERATE_DECELERATE);

        this.tempMatrix = new Matrix();
    }

    void draw(Canvas canvas, boolean quickMode) {
        draw(canvas, 255, true, quickMode);
    }

    void draw(Canvas canvas, int alpha, boolean quickMode) {
        draw(canvas, alpha, false, quickMode);
    }

    private void draw(Canvas canvas, int alpha, boolean needClip, boolean quickMode) {
        if ((drawable instanceof PixelMapElement) && !quickMode) {

            PixelMap bitmap = ((PixelMapElement) drawable).getPixelMap();
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            paint.setAlpha(alpha / 255.0f);
            int saved = canvas.saveLayer(null, paint);
            if (needClip) {
                canvas.drawPath(area.getAreaPath(), paint);
                paint.setBlendMode(BlendMode.SRC_IN);
            }
            canvas.save();
            canvas.concat(matrix);
            final RectFloat rect = new RectFloat(0, 0, bitmap.getImageInfo().size.width, bitmap.getImageInfo().size.height);
            canvas.drawPixelMapHolderRect(new PixelMapHolder(bitmap), rect, paint);
            canvas.restore();
            paint.setBlendMode(null);
            canvas.restoreToCount(saved);
        } else {
            canvas.save();
            if (needClip) {
                canvas.clipPath(area.getAreaPath(), Canvas.ClipOp.INTERSECT);
            }
            canvas.concat(matrix);
            drawable.setBounds(drawableBounds);
            drawable.setAlpha(alpha / 255);
            drawable.drawToCanvas(canvas);
            canvas.restore();
        }
    }

    public Area getArea() {
        return area;
    }

    public void setDrawable(Element drawable) {
        this.drawable = drawable;
        this.drawableBounds = new Rect(0, 0, getWidth(), getHeight());
        this.drawablePoints = new float[]{
            0f, 0f, getWidth(), 0f, getWidth(), getHeight(), 0f, getHeight()
        };
    }

    public Element getDrawable() {
        return drawable;
    }

    public int getWidth() {
        return drawable.getWidth();
    }

    public int getHeight() {
        return drawable.getHeight();
    }

    public boolean contains(float x, float y) {
        return area.contains(x, y);
    }

    public boolean contains(Line line) {
        return area.contains(line);
    }

    public Rect getDrawableBounds() {
        return drawableBounds;
    }

    void setPreviousMoveX(float previousMoveX) {
        this.previousMoveX = previousMoveX;
    }

    void setPreviousMoveY(float previousMoveY) {
        this.previousMoveY = previousMoveY;
    }

    private RectF getCurrentDrawableBounds() {
        matrix.mapRect(mappedBounds, new RectF(drawableBounds));
        return mappedBounds;
    }

    private PointF getCurrentDrawableCenterPoint() {
        getCurrentDrawableBounds();
        mappedCenterPointF.setPointX(mappedBounds.centerX());
        mappedCenterPointF.setPointY(mappedBounds.centerX());
        return mappedCenterPointF;
    }

    public PointF getAreaCenterPoint() {
        centerPointF.setPointX(area.centerX());
        centerPointF.setPointY(area.centerY());
        return centerPointF;
    }

    private float getMatrixScale() {
        return MatrixUtils.getMatrixScale(matrix);
    }

    float getMatrixAngle() {
        return MatrixUtils.getMatrixAngle(matrix);
    }

    float[] getCurrentDrawablePoints() {
        matrix.mapPoints(mappedDrawablePoints, drawablePoints);
        return mappedDrawablePoints;
    }

    boolean isFilledArea() {
        RectF bounds = getCurrentDrawableBounds();
        return !(bounds.left > area.left()
            || bounds.top > area.top()
            || bounds.right < area.right()
            || bounds.bottom < area.bottom());
    }

    boolean canFilledArea() {
        float scale = MatrixUtils.getMatrixScale(matrix);
        float minScale = MatrixUtils.getMinMatrixScale(this);
        return scale >= minScale;
    }

    void record() {
        previousMatrix.setMatrix(matrix);
    }

    void translate(float offsetX, float offsetY) {
        matrix.setMatrix(previousMatrix);
        postTranslate(offsetX, offsetY);
    }

    private void zoom(float scaleX, float scaleY, PointF midPoint) {
        matrix.setMatrix(previousMatrix);
        postScale(scaleX, scaleY, midPoint);
    }

    void zoomAndTranslate(float scaleX, float scaleY, PointF midPoint, float offsetX, float offsetY) {
        matrix.setMatrix(previousMatrix);
        postTranslate(offsetX, offsetY);
        postScale(scaleX, scaleY, midPoint);
    }

    void set(Matrix matrix) {
        this.matrix.setMatrix(matrix);
        moveToFillArea(null);
    }

    void postTranslate(float x, float y) {
        this.matrix.postTranslate(x, y);
    }

    void postScale(float scaleX, float scaleY, PointF midPoint) {
        this.matrix.postScale(scaleX, scaleY, midPoint.getPointX(), midPoint.getPointY());
    }

    void postFlipVertically() {
        this.matrix.postScale(1, -1, area.centerX(), area.centerY());
    }

    void postFlipHorizontally() {
        this.matrix.postScale(-1, 1, area.centerX(), area.centerY());
    }

    void postRotate(float degree) {
        this.matrix.postRotate(degree, area.centerX(), area.centerY());

        float minScale = getMinMatrixScale(this);
        if (getMatrixScale() < minScale) {
            final PointF midPointF = new PointF();
            midPointF.set(getCurrentDrawableCenterPoint());

            postScale(minScale / getMatrixScale(), minScale / getMatrixScale(), midPointF);
        }

        if (!judgeIsImageContainsBorder(this, getMatrixAngle())) {
            final float[] imageIndents = calculateImageIndents(this);
            float deltaX = -(imageIndents[0] + imageIndents[2]);
            float deltaY = -(imageIndents[1] + imageIndents[3]);

            postTranslate(deltaX, deltaY);
        }
    }

    private void animateTranslate(final Component view, final float translateX, final float translateY) {
        animator.end();
        animator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float v) {
                float x = translateX * v;
                float y = translateY * v;

                translate(x, y);
                view.invalidate();
            }
        });
        animator.setDuration(duration);
        animator.start();
    }

    void moveToFillArea(final Component view) {
        if (isFilledArea()) return;
        record();

        RectF rectF = getCurrentDrawableBounds();
        float offsetX = 0f;
        float offsetY = 0f;

        if (rectF.left > area.left()) {
            offsetX = area.left() - rectF.left;
        }

        if (rectF.top > area.top()) {
            offsetY = area.top() - rectF.top;
        }

        if (rectF.right < area.right()) {
            offsetX = area.right() - rectF.right;
        }

        if (rectF.bottom < area.bottom()) {
            offsetY = area.bottom() - rectF.bottom;
        }

        if (view == null) {
            postTranslate(offsetX, offsetY);
        } else {
            animateTranslate(view, offsetX, offsetY);
        }
    }

    void fillArea(final Component view, boolean quick) {
        if (isFilledArea()) return;
        record();

        final float startScale = getMatrixScale();
        final float endScale = MatrixUtils.getMinMatrixScale(this);

        final PointF midPointF = new PointF();
        midPointF.set(getCurrentDrawableCenterPoint());

        tempMatrix.setMatrix(matrix);
        tempMatrix.postScale(endScale / startScale, endScale / startScale, midPointF.getPointX(), midPointF.getPointY());

        RectF rectF = new RectF(drawableBounds);
        tempMatrix.mapRect(rectF);

        float offsetX = 0f;
        float offsetY = 0f;

        if (rectF.left > area.left()) {
            offsetX = area.left() - rectF.left;
        }

        if (rectF.top > area.top()) {
            offsetY = area.top() - rectF.top;
        }

        if (rectF.right < area.right()) {
            offsetX = area.right() - rectF.right;
        }

        if (rectF.bottom < area.bottom()) {
            offsetY = area.bottom() - rectF.bottom;
        }

        final float translateX = offsetX;
        final float translateY = offsetY;

        animator.end();
        animator.setValueUpdateListener(
            (animatorValue, vv) -> {
                float scale = (startScale + (endScale - startScale) * vv) / startScale;
                float x = translateX * vv;
                float y = translateY * vv;

                zoom(scale, scale, midPointF);
                postTranslate(x, y);
                view.invalidate();
            });
        if (quick) {
            animator.setDuration(0);
        } else {
            animator.setDuration(duration);
        }
        animator.start();
    }

    private float getTouchX(TouchEvent touchEvent, int index) {
        float getX = 0;
        if (touchEvent.getPointerCount() > index) {
            getX = touchEvent.getPointerPosition(index).getX();
        }
        return getX;
    }

    private float getTouchY(TouchEvent touchEvent, int index) {
        float getY = 0;
        if (touchEvent.getPointerCount() > index) {
            getY = touchEvent.getPointerPosition(index).getY();
        }
        return getY;
    }

    void updateWith(final TouchEvent event, final Line line) {
        float offsetX = (getTouchX(event, 0) - previousMoveX) / 2;
        float offsetY = (getTouchY(event, 0) - previousMoveY) / 2;

        if (!canFilledArea()) {
            final Area area = getArea();
            float deltaScale = MatrixUtils.getMinMatrixScale(this) / getMatrixScale();
            postScale(deltaScale, deltaScale, area.getCenterPoint());
            record();

            previousMoveX = getTouchX(event, 0);
            previousMoveY = getTouchY(event, 0);
        }

        if (line.direction() == Line.Direction.HORIZONTAL) {
            translate(0, offsetY);
        } else if (line.direction() == Line.Direction.VERTICAL) {
            translate(offsetX, 0);
        }

        final RectF rectF = getCurrentDrawableBounds();
        final Area area = getArea();
        float moveY = 0f;

        if (rectF.top > area.top()) {
            moveY = area.top() - rectF.top;
        }

        if (rectF.bottom < area.bottom()) {
            moveY = area.bottom() - rectF.bottom;
        }

        float moveX = 0f;

        if (rectF.left > area.left()) {
            moveX = area.left() - rectF.left;
        }

        if (rectF.right < area.right()) {
            moveX = area.right() - rectF.right;
        }

        if (moveX != 0 || moveY != 0) {
            previousMoveX = getTouchX(event, 0);
            previousMoveY = getTouchY(event, 0);
            postTranslate(moveX, moveY);
            record();
        }
    }

    public void setArea(Area area) {
        this.area = area;
    }

    boolean isAnimateRunning() {
        return animator.isRunning();
    }

    void setAnimateDuration(int duration) {
        this.duration = duration;
    }

    public Matrix getMatrix() {
        return matrix;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getPath() {
        return path;
    }
}
